/*
    GNU GENERAL LICENSE
    Copyright (C) 2006 The Lobo Project. Copyright (C) 2014 - 2016 Lobo Evolution

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public
    License as published by the Free Software Foundation; either
    verion 3 of the License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    General License for more details.

    You should have received a copy of the GNU General Public
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    

    Contact info: lobochief@users.sourceforge.net; ivan.difrancesco@yahoo.it
 */
/*
 * Created on Sep 3, 2005
 */
package org.lobobrowser.html.domimpl;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import org.lobobrowser.html.HtmlRendererContext;
import org.lobobrowser.html.dombl.ChildHTMLCollection;
import org.lobobrowser.html.dombl.ModelNode;
import org.lobobrowser.html.dombl.NodeVisitor;
import org.lobobrowser.html.dombl.SkipVisitorException;
import org.lobobrowser.html.dombl.StopVisitorException;
import org.lobobrowser.html.dombl.UINode;
import org.lobobrowser.html.domfilter.NodeFilter;
import org.lobobrowser.html.domfilter.TextFilter;
import org.lobobrowser.html.renderstate.RenderState;
import org.lobobrowser.html.renderstate.StyleSheetRenderState;
import org.lobobrowser.http.UserAgentContext;
import org.lobobrowser.js.AbstractScriptableDelegate;
import org.lobobrowser.util.Objects;
import org.lobobrowser.util.Strings;
import org.lobobrowser.util.Urls;
import org.w3c.dom.Attr;
import org.w3c.dom.Comment;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;
import org.w3c.dom.UserDataHandler;

/**
 * The Class DOMNodeImpl.
 */
public abstract class DOMNodeImpl extends AbstractScriptableDelegate implements
Node, ModelNode {

    /** The Constant EMPTY_ARRAY. */
    private static final DOMNodeImpl[] EMPTY_ARRAY = new DOMNodeImpl[0];

    /** The Constant INVALID_RENDER_STATE. */
    private static final RenderState INVALID_RENDER_STATE = new StyleSheetRenderState(
            null);

    /** The Constant logger. */
    protected static final Logger logger = LogManager.getLogger(DOMNodeImpl.class
            .getName());

    /** The ui node. */
    protected UINode uiNode;

    /** The node list. */
    protected ArrayList<Node> nodeList;

    /** The document. */
    protected volatile Document document;

    /**
     * A tree lock is less deadlock-prone than a node-level lock. This is
     * assigned in setOwnerDocument.
     */
    private volatile Object treeLock = this;

    /**
     * Instantiates a new DOM node impl.
     */
    public DOMNodeImpl() {
        super();
    }

    /** Sets the UI node.
	 *
	 * @param uiNode
	 *            the new UI node
	 */
    public void setUINode(UINode uiNode) {
        // Called in GUI thread always.
        this.uiNode = uiNode;
    }

    /** Gets the UI node.
	 *
	 * @return the UI node
	 */
    public UINode getUINode() {
        // Called in GUI thread always.
        return this.uiNode;
    }

    /**
     * Tries to get a UINode associated with the current node. Failing that, it
     * tries ancestors recursively. This method will return the closest
     * <i>block-level</i> renderer node, if any.
     *
     * @return the UI node
     */
    public UINode findUINode() {
        // Called in GUI thread always.
        UINode uiNode = this.uiNode;
        if (uiNode != null) {
            return uiNode;
        }
        DOMNodeImpl parentNode = (DOMNodeImpl) this.getParentNode();
        return parentNode == null ? null : parentNode.findUINode();
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#appendChild(org.w3c.dom.Node)
     */
    @Override
    public Node appendChild(Node newChild) throws DOMException {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            if (nl == null) {
                nl = new ArrayList<Node>(3);
                this.nodeList = nl;
            }
            nl.add(newChild);
            if (newChild instanceof DOMNodeImpl) {
                ((DOMNodeImpl) newChild).setParentImpl(this);
            }
        }

        if (!this.notificationsSuspended) {
            this.informStructureInvalid();
        }
        return newChild;
    }

    /**
     * Removes the all children.
     */
    protected void removeAllChildren() {
        synchronized (this.getTreeLock()) {
            this.removeAllChildrenImpl();
        }
    }

    /**
     * Removes the all children impl.
     */
    protected void removeAllChildrenImpl() {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            if (nl != null) {
                nl.clear();
                // this.nodeList = null;
            }
        }
        if (!this.notificationsSuspended) {
            this.informStructureInvalid();
        }
    }

    /**
     * Gets the node list.
     *
     * @param filter
     *            the filter
     * @return the node list
     */
    public NodeList getNodeList(NodeFilter filter) {
        Collection<DOMNodeImpl> collection = new ArrayList<DOMNodeImpl>();
        synchronized (this.getTreeLock()) {
            this.appendChildrenToCollectionImpl(filter, collection);
        }
        return new DOMNodeListImpl(collection);
    }

    /** Gets the children array.
	 *
	 * @return the children array
	 */
    public DOMNodeImpl[] getChildrenArray() {
        ArrayList<Node> nl = this.nodeList;
        synchronized (this.getTreeLock()) {
            return nl == null ? null : (DOMNodeImpl[]) nl
                    .toArray(DOMNodeImpl.EMPTY_ARRAY);
        }
    }

    /** Gets the child count.
	 *
	 * @return the child count
	 */
    public int getChildCount() {
        ArrayList<Node> nl = this.nodeList;
        synchronized (this.getTreeLock()) {
            return nl == null ? 0 : nl.size();
        }
    }

    /** The children collection. */
    private ChildHTMLCollection childrenCollection;

    /** Gets the children.
	 *
	 * @return the children
	 */
    public ChildHTMLCollection getChildren() {
        // Method required by JavaScript
        synchronized (this) {
            ChildHTMLCollection collection = this.childrenCollection;
            if (collection == null) {
                collection = new ChildHTMLCollection(this);
                this.childrenCollection = collection;
            }
            return collection;
        }
    }

    /**
     * Creates an <code>ArrayList</code> of descendent nodes that the given
     * filter condition.
     *
     * @param filter
     *            the filter
     * @param nestIntoMatchingNodes
     *            the nest into matching nodes
     * @return the descendents
     */
    public ArrayList<DOMNodeImpl> getDescendents(NodeFilter filter,
            boolean nestIntoMatchingNodes) {
        ArrayList<DOMNodeImpl> al = new ArrayList<DOMNodeImpl>();
        synchronized (this.getTreeLock()) {
            this.extractDescendentsArrayImpl(filter, al, nestIntoMatchingNodes);
        }
        return al;
    }

    /**
     * Extracts all descendents that match the filter, except those descendents
     * of nodes that match the filter.
     *
     * @param filter
     *            the filter
     * @param al
     *            the al
     * @param nestIntoMatchingNodes
     *            the nest into matching nodes
     */
    private void extractDescendentsArrayImpl(NodeFilter filter,
            ArrayList<DOMNodeImpl> al, boolean nestIntoMatchingNodes) {
        ArrayList<Node> nl = this.nodeList;
        if (nl != null) {
            Iterator<Node> i = nl.iterator();
            while (i.hasNext()) {
                DOMNodeImpl n = (DOMNodeImpl) i.next();
                if (filter.accept(n)) {
                    al.add(n);
                    if (nestIntoMatchingNodes) {
                        n.extractDescendentsArrayImpl(filter, al,
                                nestIntoMatchingNodes);
                    }
                } else if (n.getNodeType() == Node.ELEMENT_NODE) {
                    n.extractDescendentsArrayImpl(filter, al,
                            nestIntoMatchingNodes);
                }
            }
        }
    }

    /**
     * Append children to collection impl.
     *
     * @param filter
     *            the filter
     * @param collection
     *            the collection
     */
    private void appendChildrenToCollectionImpl(NodeFilter filter,
            Collection<DOMNodeImpl> collection) {
        ArrayList<Node> nl = this.nodeList;
        if (nl != null) {
            Iterator<Node> i = nl.iterator();
            while (i.hasNext()) {
                DOMNodeImpl node = (DOMNodeImpl) i.next();
                if (filter.accept(node)) {
                    collection.add(node);
                }
                node.appendChildrenToCollectionImpl(filter, collection);
            }
        }
    }

    /**
     * Should create a node with some cloned properties, like the node name, but
     * not attributes or children.
     *
     * @return the node
     */
    protected abstract Node createSimilarNode();

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#cloneNode(boolean)
     */
    @Override
    public Node cloneNode(boolean deep) {
        try {
            Node newNode = this.createSimilarNode();
            NodeList children = this.getChildNodes();
            int length = children.getLength();
            for (int i = 0; i < length; i++) {
                Node child = children.item(i);
                Node newChild = deep ? child.cloneNode(deep) : child;
                newNode.appendChild(newChild);
            }
            if (newNode instanceof Element) {
                Element elem = (Element) newNode;
                NamedNodeMap nnmap = this.getAttributes();
                if (nnmap != null) {
                    int nnlength = nnmap.getLength();
                    for (int i = 0; i < nnlength; i++) {
                        Attr attr = (Attr) nnmap.item(i);
                        elem.setAttributeNode((Attr) attr.cloneNode(true));
                    }
                }
            }

            synchronized (this) {
                if ((userDataHandlers != null) && (userData != null)) {
                    for (Iterator handlers = userDataHandlers.entrySet()
                            .iterator(); handlers.hasNext();) {
                        Map.Entry entry = (Map.Entry) handlers.next();
                        UserDataHandler handler = (UserDataHandler) entry
                                .getValue();
                        handler.handle(UserDataHandler.NODE_CLONED,
                                (String) entry.getKey(),
                                userData.get(entry.getKey()), this, newNode);
                    }
                }
            }

            return newNode;
        } catch (Exception err) {
            throw new IllegalStateException(err.getMessage());
        }
    }

    /** Gets the node index.
	 *
	 * @return the node index
	 */
    private int getNodeIndex() {
        DOMNodeImpl parent = (DOMNodeImpl) this.getParentNode();
        return parent == null ? -1 : parent.getChildIndex(this);
    }

    /**
     * Gets the child index.
     *
     * @param child
     *            the child
     * @return the child index
     */
    public int getChildIndex(Node child) {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            return nl == null ? -1 : nl.indexOf(child);
        }
    }

    /**
     * Gets the child at index.
     *
     * @param index
     *            the index
     * @return the child at index
     */
    public Node getChildAtIndex(int index) {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            try {
                return nl == null ? null : (Node) nl.get(index);
            } catch (IndexOutOfBoundsException iob) {
            	logger.error("getChildAtIndex(): Bad index=" + index
                        + " for node=" + this + ".");
                return null;
            }
        }
    }

    /**
     * Checks if is ancestor of.
     *
     * @param other
     *            the other
     * @return true, if is ancestor of
     */
    private boolean isAncestorOf(Node other) {
        DOMNodeImpl parent = (DOMNodeImpl) other.getParentNode();
        if (parent == this) {
            return true;
        } else if (parent == null) {
            return false;
        } else {
            return this.isAncestorOf(parent);
        }
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#compareDocumentPosition(org.w3c.dom.Node)
     */
    @Override
    public short compareDocumentPosition(Node other) throws DOMException {
        Node parent = this.getParentNode();
        if (!(other instanceof DOMNodeImpl)) {
            throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
                    "Unknwon node implementation");
        }
        if ((parent != null) && (parent == other.getParentNode())) {
            int thisIndex = this.getNodeIndex();
            int otherIndex = ((DOMNodeImpl) other).getNodeIndex();
            if ((thisIndex == -1) || (otherIndex == -1)) {
                return Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC;
            }
            if (thisIndex < otherIndex) {
                return Node.DOCUMENT_POSITION_FOLLOWING;
            } else {
                return Node.DOCUMENT_POSITION_PRECEDING;
            }
        } else if (this.isAncestorOf(other)) {
            return Node.DOCUMENT_POSITION_CONTAINED_BY;
        } else if (((DOMNodeImpl) other).isAncestorOf(this)) {
            return Node.DOCUMENT_POSITION_CONTAINS;
        } else {
            return Node.DOCUMENT_POSITION_DISCONNECTED;
        }
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getAttributes()
     */
    @Override
    public NamedNodeMap getAttributes() {
        return null;
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getOwnerDocument()
     */
    @Override
    public Document getOwnerDocument() {
        return this.document;
    }

    /** Sets the owner document.
	 *
	 * @param value
	 *            the new owner document
	 */
    public void setOwnerDocument(Document value) {
        this.document = value;
        this.setTreeLock(value == null ? this : (Object) value);
    }

    /**
     * Sets the owner document.
     *
     * @param value
     *            the value
     * @param deep
     *            the deep
     */
    public void setOwnerDocument(Document value, boolean deep) {
        this.document = value;
        this.setTreeLock(value == null ? this : (Object) value);
        if (deep) {
            synchronized (this.getTreeLock()) {
                ArrayList<Node> nl = this.nodeList;
                if (nl != null) {
                    Iterator<Node> i = nl.iterator();
                    while (i.hasNext()) {
                        DOMNodeImpl child = (DOMNodeImpl) i.next();
                        child.setOwnerDocument(value, deep);
                    }
                }
            }
        }
    }

    /**
     * Visit impl.
     *
     * @param visitor
     *            the visitor
     */
    protected void visitImpl(NodeVisitor visitor) {
        try {
            visitor.visit(this);
        } catch (SkipVisitorException sve) {
            return;
        } catch (StopVisitorException sve) {
            throw sve;
        }
        ArrayList<Node> nl = this.nodeList;
        if (nl != null) {
            Iterator<Node> i = nl.iterator();
            while (i.hasNext()) {
                DOMNodeImpl child = (DOMNodeImpl) i.next();
                try {
                    // Call with child's synchronization
                    child.visit(visitor);
                } catch (StopVisitorException sve) {
                    throw sve;
                }
            }
        }
    }

    /**
     * Visit.
     *
     * @param visitor
     *            the visitor
     */
    public void visit(NodeVisitor visitor) {
        synchronized (this.getTreeLock()) {
            this.visitImpl(visitor);
        }
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#insertBefore(org.w3c.dom.Node, org.w3c.dom.Node)
     */
    @Override
    public Node insertBefore(Node newChild, Node refChild) throws DOMException {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            int idx = nl == null ? -1 : nl.indexOf(refChild);
            if (idx == -1) {
                throw new DOMException(DOMException.NOT_FOUND_ERR,
                        "refChild not found");
            }
            nl.add(idx, newChild);
            if (newChild instanceof DOMNodeImpl) {
                ((DOMNodeImpl) newChild).setParentImpl(this);
            }
        }
        if (!this.notificationsSuspended) {
            this.informStructureInvalid();
        }
        return newChild;
    }

    /**
     * Insert at.
     *
     * @param newChild
     *            the new child
     * @param idx
     *            the idx
     * @return the node
     * @throws DOMException
     *             the DOM exception
     */
    protected Node insertAt(Node newChild, int idx) throws DOMException {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            if (nl == null) {
                nl = new ArrayList<Node>();
                this.nodeList = nl;
            }
            nl.add(idx, newChild);
            if (newChild instanceof DOMNodeImpl) {
                ((DOMNodeImpl) newChild).setParentImpl(this);
            }
        }
        if (!this.notificationsSuspended) {
            this.informStructureInvalid();
        }
        return newChild;
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#replaceChild(org.w3c.dom.Node, org.w3c.dom.Node)
     */
    @Override
    public Node replaceChild(Node newChild, Node oldChild) throws DOMException {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            int idx = nl == null ? -1 : nl.indexOf(oldChild);
            if (idx == -1) {
                throw new DOMException(DOMException.NOT_FOUND_ERR,
                        "oldChild not found");
            }
            nl.set(idx, newChild);
        }
        if (!this.notificationsSuspended) {
            this.informStructureInvalid();
        }
        return newChild;
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#removeChild(org.w3c.dom.Node)
     */
    @Override
    public Node removeChild(Node oldChild) throws DOMException {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            if ((nl == null) || !nl.remove(oldChild)) {
                throw new DOMException(DOMException.NOT_FOUND_ERR,
                        "oldChild not found");
            }
        }
        if (!this.notificationsSuspended) {
            this.informStructureInvalid();
        }
        return oldChild;
    }

    /**
     * Removes the child at.
     *
     * @param index
     *            the index
     * @return the node
     * @throws DOMException
     *             the DOM exception
     */
    public Node removeChildAt(int index) throws DOMException {
        try {
            synchronized (this.getTreeLock()) {
                ArrayList<Node> nl = this.nodeList;
                if (nl == null) {
                    throw new DOMException(DOMException.INDEX_SIZE_ERR,
                            "Empty list of children");
                }
                Node n = nl.remove(index);
                if (n == null) {
                    throw new DOMException(DOMException.INDEX_SIZE_ERR,
                            "No node with that index");
                }
                return n;
            }
        } finally {
            if (!this.notificationsSuspended) {
                this.informStructureInvalid();
            }
        }
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#hasChildNodes()
     */
    @Override
    public boolean hasChildNodes() {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            return (nl != null) && !nl.isEmpty();
        }
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getBaseURI()
     */
    @Override
    public String getBaseURI() {
        Document document = this.document;
        return document == null ? null : document.getBaseURI();
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getChildNodes()
     */
    @Override
    public NodeList getChildNodes() {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            return new DOMNodeListImpl(nl == null ? Collections.EMPTY_LIST : nl);
        }
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getFirstChild()
     */
    @Override
    public Node getFirstChild() {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            try {
                return nl == null ? null : (Node) nl.get(0);
            } catch (IndexOutOfBoundsException iob) {
                return null;
            }
        }
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getLastChild()
     */
    @Override
    public Node getLastChild() {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            try {
                return nl == null ? null : (Node) nl.get(nl.size() - 1);
            } catch (IndexOutOfBoundsException iob) {
                return null;
            }
        }
    }

    /**
     * Gets the previous to.
     *
     * @param node
     *            the node
     * @return the previous to
     */
    private Node getPreviousTo(Node node) {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            int idx = nl == null ? -1 : nl.indexOf(node);
            if (idx == -1) {
                throw new DOMException(DOMException.NOT_FOUND_ERR,
                        "node not found");
            }
            try {
                return nl.get(idx - 1);
            } catch (IndexOutOfBoundsException iob) {
                return null;
            }
        }
    }

    /**
     * Gets the next to.
     *
     * @param node
     *            the node
     * @return the next to
     */
    private Node getNextTo(Node node) {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            int idx = nl == null ? -1 : nl.indexOf(node);
            if (idx == -1) {
                throw new DOMException(DOMException.NOT_FOUND_ERR,
                        "node not found");
            }
            try {
                return nl.get(idx + 1);
            } catch (IndexOutOfBoundsException iob) {
                return null;
            }
        }
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getPreviousSibling()
     */
    @Override
    public Node getPreviousSibling() {
        DOMNodeImpl parent = (DOMNodeImpl) this.getParentNode();
        return parent == null ? null : parent.getPreviousTo(this);
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getNextSibling()
     */
    @Override
    public Node getNextSibling() {
        DOMNodeImpl parent = (DOMNodeImpl) this.getParentNode();
        return parent == null ? null : parent.getNextTo(this);
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getFeature(java.lang.String, java.lang.String)
     */
    @Override
    public Object getFeature(String feature, String version) {
        // TODO What should this do?
        return null;
    }

    /** The user data. */
    private Map<String, Object> userData;
    // TODO: Inform handlers on cloning, etc.
    /** The user data handlers. */
    private Map<String, UserDataHandler> userDataHandlers;

    /** The notifications suspended. */
    protected volatile boolean notificationsSuspended = false;

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#setUserData(java.lang.String, java.lang.Object,
     * org.w3c.dom.UserDataHandler)
     */
    @Override
    public Object setUserData(String key, Object data, UserDataHandler handler) {
        if (org.lobobrowser.html.parser.HtmlParser.MODIFYING_KEY.equals(key)) {
            boolean ns = (Boolean.TRUE == data);
            this.notificationsSuspended = ns;
            if (!ns) {
                this.informNodeLoaded();
            }
        }
        // here we spent some effort preventing our maps from growing too much
        synchronized (this) {
            if (handler != null) {
                if (this.userDataHandlers == null) {
                    this.userDataHandlers = new HashMap<String, UserDataHandler>();
                } else {
                    this.userDataHandlers.put(key, handler);
                }
            }

            Map<String, Object> userData = this.userData;
            if (data != null) {
                if (userData == null) {
                    userData = new HashMap<String, Object>();
                    this.userData = userData;
                }
                return userData.put(key, data);
            } else if (userData != null) {
                return userData.remove(key);
            } else {
                return null;
            }
        }
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getUserData(java.lang.String)
     */
    @Override
    public Object getUserData(String key) {
        synchronized (this) {
            Map<String, Object> ud = this.userData;
            return ud == null ? null : ud.get(key);
        }
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getLocalName()
     */
    @Override
    public abstract String getLocalName();

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#hasAttributes()
     */
    @Override
    public boolean hasAttributes() {
        return false;
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getNamespaceURI()
     */
    @Override
    public String getNamespaceURI() {
        return null;
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getNodeName()
     */
    @Override
    public abstract String getNodeName();

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getNodeValue()
     */
    @Override
    public abstract String getNodeValue() throws DOMException;

    /** The prefix. */
    private volatile String prefix;

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getPrefix()
     */
    @Override
    public String getPrefix() {
        return this.prefix;
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#setPrefix(java.lang.String)
     */
    @Override
    public void setPrefix(String prefix) throws DOMException {
        this.prefix = prefix;
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#setNodeValue(java.lang.String)
     */
    @Override
    public abstract void setNodeValue(String nodeValue) throws DOMException;

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getNodeType()
     */
    @Override
    public abstract short getNodeType();

    /**
     * Gets the text content of this node and its descendents.
     *
     * @return the text content
     * @throws DOMException
     *             the DOM exception
     */
    @Override
    public String getTextContent() throws DOMException {
        StringBuffer sb = new StringBuffer();
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            if (nl != null) {
                Iterator<Node> i = nl.iterator();
                while (i.hasNext()) {
                    Node node = i.next();
                    short type = node.getNodeType();
                    switch (type) {
                    case Node.CDATA_SECTION_NODE:
                    case Node.TEXT_NODE:
                    case Node.ELEMENT_NODE:
                        String textContent = node.getTextContent();
                        if (textContent != null) {
                            sb.append(textContent);
                        }
                        break;
                    default:
                        break;
                    }
                }
            }
        }
        return sb.toString();
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#setTextContent(java.lang.String)
     */
    @Override
    public void setTextContent(String textContent) throws DOMException {
        synchronized (this.getTreeLock()) {
            this.removeChildrenImpl(new TextFilter());
            if ((textContent != null) && !"".equals(textContent)) {
                DOMTextImpl t = new DOMTextImpl(textContent);
                t.setOwnerDocument(this.document);
                t.setParentImpl(this);
                ArrayList<Node> nl = this.nodeList;
                if (nl == null) {
                    nl = new ArrayList<Node>();
                    this.nodeList = nl;
                }
                nl.add(t);
            }
        }
        if (!this.notificationsSuspended) {
            this.informStructureInvalid();
        }
    }

    /**
     * Removes the children.
     *
     * @param filter
     *            the filter
     */
    protected void removeChildren(NodeFilter filter) {
        synchronized (this.getTreeLock()) {
            this.removeChildrenImpl(filter);
        }
        if (!this.notificationsSuspended) {
            this.informStructureInvalid();
        }
    }

    /**
     * Removes the children impl.
     *
     * @param filter
     *            the filter
     */
    protected void removeChildrenImpl(NodeFilter filter) {
        ArrayList<Node> nl = this.nodeList;
        if (nl != null) {
            int len = nl.size();
            for (int i = len;--i >= 0;) {
                Node node = nl.get(i);
                if (filter.accept(node)) {
                    nl.remove(i);
                }
            }
        }
    }

    /**
     * Insert after.
     *
     * @param newChild
     *            the new child
     * @param refChild
     *            the ref child
     * @return the node
     */
    public Node insertAfter(Node newChild, Node refChild) {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            int idx = nl == null ? -1 : nl.indexOf(refChild);
            if (idx == -1) {
                throw new DOMException(DOMException.NOT_FOUND_ERR,
                        "refChild not found");
            }
            nl.add(idx + 1, newChild);
            if (newChild instanceof DOMNodeImpl) {
                ((DOMNodeImpl) newChild).setParentImpl(this);
            }
        }
        if (!this.notificationsSuspended) {
            this.informStructureInvalid();
        }
        return newChild;
    }

    /**
     * Replace adjacent text nodes.
     *
     * @param node
     *            the node
     * @param textContent
     *            the text content
     * @return the text
     */
    public Text replaceAdjacentTextNodes(Text node, String textContent) {
        try {
            synchronized (this.getTreeLock()) {
                ArrayList<Node> nl = this.nodeList;
                if (nl == null) {
                    throw new DOMException(DOMException.NOT_FOUND_ERR,
                            "Node not a child");
                }
                int idx = nl.indexOf(node);
                if (idx == -1) {
                    throw new DOMException(DOMException.NOT_FOUND_ERR,
                            "Node not a child");
                }
                int firstIdx = idx;
                List<Object> toDelete = new LinkedList<Object>();
                for (int adjIdx = idx;--adjIdx >= 0;) {
                    Object child = this.nodeList.get(adjIdx);
                    if (child instanceof Text) {
                        firstIdx = adjIdx;
                        toDelete.add(child);
                    }
                }
                int length = this.nodeList.size();
                for (int adjIdx = idx; ++adjIdx < length;) {
                    Object child = this.nodeList.get(adjIdx);
                    if (child instanceof Text) {
                        toDelete.add(child);
                    }
                }
                this.nodeList.removeAll(toDelete);
                DOMTextImpl textNode = new DOMTextImpl(textContent);
                textNode.setOwnerDocument(this.document);
                textNode.setParentImpl(this);
                this.nodeList.add(firstIdx, textNode);
                return textNode;
            }
        } finally {
            if (!this.notificationsSuspended) {
                this.informStructureInvalid();
            }
        }
    }

    /**
     * Replace adjacent text nodes.
     *
     * @param node
     *            the node
     * @return the text
     */
    public Text replaceAdjacentTextNodes(Text node) {
        try {
            synchronized (this.getTreeLock()) {
                ArrayList<Node> nl = this.nodeList;
                if (nl == null) {
                    throw new DOMException(DOMException.NOT_FOUND_ERR,
                            "Node not a child");
                }
                int idx = nl.indexOf(node);
                if (idx == -1) {
                    throw new DOMException(DOMException.NOT_FOUND_ERR,
                            "Node not a child");
                }
                StringBuffer textBuffer = new StringBuffer();
                int firstIdx = idx;
                List<Object> toDelete = new LinkedList<Object>();
                for (int adjIdx = idx;--adjIdx >= 0;) {
                    Object child = this.nodeList.get(adjIdx);
                    if (child instanceof Text) {
                        firstIdx = adjIdx;
                        toDelete.add(child);
                        textBuffer.append(((Text) child).getNodeValue());
                    }
                }
                int length = this.nodeList.size();
                for (int adjIdx = idx; ++adjIdx < length;) {
                    Object child = this.nodeList.get(adjIdx);
                    if (child instanceof Text) {
                        toDelete.add(child);
                        textBuffer.append(((Text) child).getNodeValue());
                    }
                }
                this.nodeList.removeAll(toDelete);
                DOMTextImpl textNode = new DOMTextImpl(textBuffer.toString());
                textNode.setOwnerDocument(this.document);
                textNode.setParentImpl(this);
                this.nodeList.add(firstIdx, textNode);
                return textNode;
            }
        } finally {
            if (!this.notificationsSuspended) {
                this.informStructureInvalid();
            }
        }
    }

    /** The parent node. */
    protected volatile Node parentNode;

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#getParentNode()
     */
    @Override
    public Node getParentNode() {
        // Should it be synchronized? Could have side-effects.
        return this.parentNode;
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#isSameNode(org.w3c.dom.Node)
     */
    @Override
    public boolean isSameNode(Node other) {
        return this == other;
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#isSupported(java.lang.String, java.lang.String)
     */
    @Override
    public boolean isSupported(String feature, String version) {
        return ("HTML".equals(feature) && (version.compareTo("4.01") <= 0));
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#lookupNamespaceURI(java.lang.String)
     */
    @Override
    public String lookupNamespaceURI(String prefix) {
        return null;
    }

    /**
     * Equal attributes.
     *
     * @param arg
     *            the arg
     * @return true, if successful
     */
    public boolean equalAttributes(Node arg) {
        return false;
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#isEqualNode(org.w3c.dom.Node)
     */
    @Override
    public boolean isEqualNode(Node arg) {
        return (arg instanceof DOMNodeImpl)
                && (this.getNodeType() == arg.getNodeType())
                && Objects.equals(this.getNodeName(), arg.getNodeName())
                && Objects.equals(this.getNodeValue(), arg.getNodeValue())
                && Objects.equals(this.getLocalName(), arg.getLocalName())
                && Objects.equals(this.nodeList, ((DOMNodeImpl) arg).nodeList)
                && this.equalAttributes(arg);
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#isDefaultNamespace(java.lang.String)
     */
    @Override
    public boolean isDefaultNamespace(String namespaceURI) {
        return namespaceURI == null;
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#lookupPrefix(java.lang.String)
     */
    @Override
    public String lookupPrefix(String namespaceURI) {
        return null;
    }

    /*
     * (non-Javadoc)
     * @see org.w3c.dom.Node#normalize()
     */
    @Override
    public void normalize() {
        synchronized (this.getTreeLock()) {
            ArrayList<Node> nl = this.nodeList;
            if (nl != null) {
                Iterator<Node> i = nl.iterator();
                List<Node> textNodes = new LinkedList<Node>();
                boolean prevText = false;
                while (i.hasNext()) {
                    Node child = i.next();
                    if (child.getNodeType() == Node.TEXT_NODE) {
                        if (!prevText) {
                            prevText = true;
                            textNodes.add(child);
                        }
                    } else {
                        prevText = false;
                    }
                }
                i = textNodes.iterator();
                while (i.hasNext()) {
                    Text text = (Text) i.next();
                    this.replaceAdjacentTextNodes(text);
                }
            }
        }
        if (!this.notificationsSuspended) {
            this.informStructureInvalid();
        }
    }

    /*
     * (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return this.getNodeName();
    }

    /** Gets the user agent context.
	 *
	 * @return the user agent context
	 */
    public UserAgentContext getUserAgentContext() {
        Object doc = this.document;
        if (doc instanceof HTMLDocumentImpl) {
            return ((HTMLDocumentImpl) doc).getUserAgentContext();
        } else {
            return null;
        }
    }

    /** Gets the html renderer context.
	 *
	 * @return the html renderer context
	 */
    public HtmlRendererContext getHtmlRendererContext() {
        Object doc = this.document;
        if (doc instanceof HTMLDocumentImpl) {
            return ((HTMLDocumentImpl) doc).getHtmlRendererContext();
        } else {
            return null;
        }
    }

    /** Sets the parent impl.
	 *
	 * @param parent
	 *            the new parent impl
	 */
    protected final void setParentImpl(Node parent) {
        // Call holding treeLock.
        this.parentNode = parent;
    }

    //-----ModelNode implementation

    /*
     * (non-Javadoc)
     * @see org.lobobrowser.html.render.RenderableContext#getAlignmentX()
     */
    /** Gets the alignment x.
	 *
	 * @return the alignment x
	 */
    public float getAlignmentX() {
        // TODO: Removable method?
        return 0.5f;
    }

    /*
     * (non-Javadoc)
     * @see org.lobobrowser.html.render.RenderableContext#getAlignmentY()
     */
    /** Gets the alignment y.
	 *
	 * @return the alignment y
	 */
    public float getAlignmentY() {
        return 0.5f;
    }

    /*
     * (non-Javadoc)
     * @see org.lobobrowser.html.render.RenderableContext#getFullURL(String)
     */
    @Override
    public URL getFullURL(String spec) throws MalformedURLException {
        Object doc = this.document;
        String cleanSpec = Urls.encodeIllegalCharacters(spec);
        if (doc instanceof HTMLDocumentImpl) {
            return ((HTMLDocumentImpl) doc).getFullURL(cleanSpec);
        } else {
            return new URL(cleanSpec);
        }
    }

    /** Gets the document url.
	 *
	 * @return the document url
	 */
    public URL getDocumentURL() {
        Object doc = this.document;
        if (doc instanceof HTMLDocumentImpl) {
            return ((HTMLDocumentImpl) doc).getDocumentURL();
        } else {
            return null;
        }
    }

    /*
     * (non-Javadoc)
     * @see org.lobobrowser.html.render.RenderableContext#getDocumentItem( String)
     */
    @Override
    public Object getDocumentItem(String name) {
        org.w3c.dom.Document document = this.document;
        return document == null ? null : document.getUserData(name);
    }

    /*
     * (non-Javadoc)
     * @see org.lobobrowser.html.render.RenderableContext#setDocumentItem( String,
     * java.lang.Object)
     */
    @Override
    public void setDocumentItem(String name, Object value) {
        org.w3c.dom.Document document = this.document;
        if (document == null) {
            return;
        }
        document.setUserData(name, value, null);
    }

    /*
     * (non-Javadoc)
     * @see org.lobobrowser.html.render.RenderableContext#isEqualOrDescendentOf(org.
     * xamjwg.html.renderer.RenderableContext)
     */
    @Override
    public final boolean isEqualOrDescendentOf(ModelNode otherContext) {
        if (otherContext == this) {
            return true;
        }
        Object parent = this.getParentNode();
        if (parent instanceof HTMLElementImpl) {
            return ((HTMLElementImpl) parent)
                    .isEqualOrDescendentOf(otherContext);
        } else {
            return false;
        }
    }

    /*
     * (non-Javadoc)
     * @see org.lobobrowser.html.dombl.ModelNode#getParentModelNode()
     */
    @Override
    public final ModelNode getParentModelNode() {
        return (ModelNode) this.parentNode;
    }

    /**
     * Inform size invalid.
     */
    public void informSizeInvalid() {
        HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document;
        if (doc != null) {
            doc.sizeInvalidated(this);
        }
    }

    /**
     * Inform look invalid.
     */
    public void informLookInvalid() {
        this.forgetRenderState();
        HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document;
        if (doc != null) {
            doc.lookInvalidated(this);
        }
    }

    /**
     * Inform position invalid.
     */
    public void informPositionInvalid() {
        HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document;
        if (doc != null) {
            doc.positionInParentInvalidated(this);
        }
    }

    /**
     * Inform invalid.
     */
    public void informInvalid() {
        // This is called when an attribute or child changes.
        this.forgetRenderState();
        HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document;
        if (doc != null) {
            doc.invalidated(this);
        }
    }

    /**
     * Inform structure invalid.
     */
    public void informStructureInvalid() {
        // This is called when an attribute or child changes.
        this.forgetRenderState();
        HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document;
        if (doc != null) {
            doc.structureInvalidated(this);
        }
    }

    /**
     * Inform node loaded.
     */
    protected void informNodeLoaded() {
        // This is called when an attribute or child changes.
        this.forgetRenderState();
        HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document;
        if (doc != null) {
            doc.nodeLoaded(this);
        }
    }

    /**
     * Inform external script loading.
     */
    protected void informExternalScriptLoading() {
        // This is called when an attribute or child changes.
        this.forgetRenderState();
        HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document;
        if (doc != null) {
            doc.externalScriptLoading(this);
        }
    }

    /**
     * Inform layout invalid.
     */
    public void informLayoutInvalid() {
        // This is called by the style properties object.
        this.forgetRenderState();
        HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document;
        if (doc != null) {
            doc.invalidated(this);
        }
    }

    /**
     * Inform document invalid.
     */
    public void informDocumentInvalid() {
        // This is called when an attribute or child changes.
        HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document;
        if (doc != null) {
            doc.allInvalidated(true);
        }
    }

    /** The render state. */
    private RenderState renderState = INVALID_RENDER_STATE;

    /*
     * (non-Javadoc)
     * @see org.lobobrowser.html.dombl.ModelNode#getRenderState()
     */
    @Override
    public RenderState getRenderState() {
        // Generally called from the GUI thread, except for
        // offset properties.
        RenderState rs;
        synchronized (this.getTreeLock()) {
            rs = this.renderState;
            if (rs != INVALID_RENDER_STATE) {
                return rs;
            }
            Object parent = this.parentNode;
            if ((parent != null) || (this instanceof Document)) {
                RenderState prs = this.getParentRenderState(parent);
                rs = this.createRenderState(prs);
                this.renderState = rs;
                return rs;
            } else {
                // Return null without caching.
                // Scenario is possible due to Javascript.
                return null;
            }
        }
    }

    /**
     * Gets the parent render state.
     *
     * @param parent
     *            the parent
     * @return the parent render state
     */
    protected final RenderState getParentRenderState(Object parent) {
        if (parent instanceof DOMNodeImpl) {
            return ((DOMNodeImpl) parent).getRenderState();
        } else {
            return null;
        }
    }

    /**
     * Creates the render state.
     *
     * @param prevRenderState
     *            the prev render state
     * @return the render state
     */
    protected RenderState createRenderState(RenderState prevRenderState) {
        return prevRenderState;
    }

    /**
     * Forget render state.
     */
    protected void forgetRenderState() {
        synchronized (this.getTreeLock()) {
            if (this.renderState != INVALID_RENDER_STATE) {
                this.renderState = INVALID_RENDER_STATE;
                // Note that getRenderState() "validates"
                // ancestor states as well.
                ArrayList<Node> nl = this.nodeList;
                if (nl != null) {
                    Iterator<Node> i = nl.iterator();
                    while (i.hasNext()) {
                        ((DOMNodeImpl) i.next()).forgetRenderState();
                    }
                }
            }
        }
    }

    /** Gets the inner html.
	 *
	 * @return the inner html
	 */
    public String getInnerHTML() {
        StringBuffer buffer = new StringBuffer();
        synchronized (this) {
            this.appendInnerHTMLImpl(buffer);
        }
        return buffer.toString();
    }

    /**
     * Append inner html impl.
     *
     * @param buffer
     *            the buffer
     */
    protected void appendInnerHTMLImpl(StringBuffer buffer) {
        ArrayList<Node> nl = this.nodeList;
        int size;
        if ((nl != null) && ((size = nl.size()) > 0)) {
            for (int i = 0; i < size; i++) {
                Node child = nl.get(i);
                if (child instanceof HTMLElementImpl) {
                    ((HTMLElementImpl) child).appendOuterHTMLImpl(buffer);
                } else if (child instanceof Comment) {
                    buffer.append("<!--" + ((Comment) child).getTextContent()
                            + "-->");
                } else if (child instanceof Text) {
                    String text = ((Text) child).getTextContent();
                    String encText = this.htmlEncodeChildText(text);
                    buffer.append(encText);
                } else if (child instanceof ProcessingInstruction) {
                    buffer.append(child.toString());
                }
            }
        }
    }

    /**
     * Html encode child text.
     *
     * @param text
     *            the text
     * @return the string
     */
    protected String htmlEncodeChildText(String text) {
        return Strings.strictHtmlEncode(text, false);
    }

    /** Gets the inner text.
	 *
	 * @return the inner text
	 */
    public String getInnerText() {
        StringBuffer buffer = new StringBuffer();
        synchronized (this.getTreeLock()) {
            this.appendInnerTextImpl(buffer);
        }
        return buffer.toString();
    }

    /**
     * Append inner text impl.
     *
     * @param buffer
     *            the buffer
     */
    protected void appendInnerTextImpl(StringBuffer buffer) {
        ArrayList<Node> nl = this.nodeList;
        if (nl == null) {
            return;
        }
        int size = nl.size();
        if (size == 0) {
            return;
        }
        for (int i = 0; i < size; i++) {
            Node child = nl.get(i);
            if (child instanceof DOMElementImpl) {
                ((DOMElementImpl) child).appendInnerTextImpl(buffer);
            }
            if (child instanceof Comment) {
                // skip
            } else if (child instanceof Text) {
                buffer.append(((Text) child).getTextContent());
            }
        }
    }

    /** Gets the a tree lock is less deadlock-prone than a node-level lock.
	 *
	 * @return the a tree lock is less deadlock-prone than a node-level lock
	 */
    public Object getTreeLock() {
        return treeLock;
    }

    /** Sets the a tree lock is less deadlock-prone than a node-level lock.
	 *
	 * @param treeLock
	 *            the new a tree lock is less deadlock-prone than a node-level
	 *            lock
	 */
    public void setTreeLock(Object treeLock) {
        this.treeLock = treeLock;
    }
}
